/*
    Priscilla - A library for enocean network access
    Copyright (C) 2012-2013 B. Aigner / R. Wagner

    This program is free software; you can redistribute it and/or 
    modify it under the terms of the GNU General Public License 
    as published by the Free Software Foundation; either version 2 
    of the License, or at your option any later version. 
 
    This program is distributed in the hope that it will be useful, 
    but WITHOUT ANY WARRANTY; without even the implied warranty of 
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    GNU General Public License for more details. 
 
    You should have received a copy of the GNU General Public License 
    along with this program; if not, write to the Free Software 
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. 
 
    Linking this library statically or dynamically with other modules is 
    making a combined work based on this library. Thus, the terms and 
    conditions of the GNU General Public License cover the whole 
    combination. 
 
    As a special exception, the copyright holders of this library give you 
    permission to link this library with independent modules to produce an 
    executable, regardless of the license terms of these independent 
    modules, and to copy and distribute the resulting executable under terms 
    of your choice, provided that you also meet, for each linked independent 
    module, the terms and conditions of the license of that module. An 
    independent module is a module which is not derived from or based on 
    this library. If you modify this library, you may extend this exception 
    to your version of the library, but you are not obligated to do so. If 
    you do not wish to do so, delete this exception statement from your 
    version. 
*/

package at.technikum_wien.embsys.aat.PriscillaCore.parsing;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;


import org.apache.log4j.Logger;
import org.xml.sax.SAXException;

import at.technikum_wien.embsys.aat.PriscillaCore.component.EEP_05_xx_xx;
import at.technikum_wien.embsys.aat.PriscillaCore.component.EEP_06_xx_xx;
import at.technikum_wien.embsys.aat.PriscillaCore.component.EnoceanComponent;
import at.technikum_wien.embsys.aat.PriscillaCore.component.ErrorMsg;
import at.technikum_wien.embsys.aat.PriscillaCore.component.GateWayID;
import at.technikum_wien.embsys.aat.PriscillaCore.component.LearnTelegram;
import at.technikum_wien.embsys.aat.PriscillaCore.data.EnoceanRawData;
import at.technikum_wien.embsys.aat.PriscillaCore.enums.ErrorCode;
import at.technikum_wien.embsys.aat.PriscillaCore.enums.ErrorCodeLRN;
import at.technikum_wien.embsys.aat.PriscillaCore.enums.TeachInOptions;
import at.technikum_wien.embsys.aat.PriscillaCore.exception.EnOceanExceptionDeviceUnknown;
import at.technikum_wien.embsys.aat.PriscillaCore.exception.EnOceanExceptionFormat;
import at.technikum_wien.embsys.aat.PriscillaCore.exception.EnOceanExceptionLenght;
import at.technikum_wien.embsys.aat.PriscillaCore.link.EnOceanLinkImpl;
import at.technikum_wien.embsys.aat.PriscillaCore.telegram.Device;
import at.technikum_wien.embsys.aat.PriscillaCore.telegram.Telegram;
import at.technikum_wien.embsys.aat.PriscillaCore.telegram.TelegramESP3;
import at.technikum_wien.embsys.aat.PriscillaCore.xml.DeviceListXML;

/**
 * Parsing an EnoceanRawFrame, an Enocean component will be returned for use.
 * 
 * This class gets an EnoceanRawFrame and parses it. As result, the corresponding
 * component is returned. For RPS and 1BS telegrams, the component selection is fixed,
 * because there are not so much different device types. For 4BS telegrams, the TYPE and
 * FUNC fields are read out from the database (if an entry exists) and the component class 
 * is loaded dynamically by the EEP string (e.g. EEP_07_10_01 for a room operating panel with
 * ORG 0x07, FUNC 0x10 and TYPE 0x01).
 *  
 * @author R. Wagner
 * @author B. Aigner
 * @version 1.0
 */
public class EnoceanComponentParser {

	/**
	 * Saving the raw data
	 */
	EnoceanRawData enoceanRawData;

	/**
	 * List of known devices from the devices.xml file
	 */
	List<Device> list;

	/**
	 * List of event listeners, which are subscribed to the events
	 */
	private List<?> listeners;

	/**
	 * Configuration of the teach-in process
	 */
	private TeachInOptions options;
	
	/**
	 * Logger for log4j
	 */
	private static final Logger log = Logger.getLogger(EnoceanComponentParser.class);;

	/**
	 * Path to devices file
	 */
	private File devicesFile = new File(DeviceListXML.FILENAME_DEFAULT);

	/**
	 * Empty constructor, no parsing will be done
	 */
	public EnoceanComponentParser() {
		super();
	}

	/**
	 * No parsing will be done, the devices file will be specified
	 * 
	 * @param devicesFile File to save device data
	 */
	public EnoceanComponentParser(File devicesFile) {
		this();

		if (devicesFile != null) {
			this.devicesFile = devicesFile;
		}
	}
	
	/**
	 * No parsing will be done, the event listeners will be saved
	 * 
	 * @param listeners List of event subscribers
	 * @param devicesFile File to save device data
	 */
	public EnoceanComponentParser(File devicesFile, List<?> listeners) {
		this(listeners);

		if (devicesFile != null) {
			this.devicesFile = devicesFile;
		}
	}
	
	/**
	 * No parsing will be done, the event listeners will be saved
	 * 
	 * @param listeners List of event subscribers
	 */
	public EnoceanComponentParser(List<?> listeners) {
		super();
		this.listeners = listeners;
	}
	
	/**
	 * No parsing will be done, the event listeners and the teach-in options will be saved
	 * 
	 * @param listeners List of event subsribers
	 * @param options Options for the teach-in process
	 */
	public EnoceanComponentParser(List<?> listeners, TeachInOptions options) {
		super();
		this.options = options;
		this.listeners = listeners;
	}

	/**
	 * Parse the given EnoceanRawData and return the corresponding component
	 * 
	 * This method checks the EnoceanRawData for length and matching sync header.
	 * The actual device list is read from the DeviceListXML class.
	 * The ORG byte is read out and different components are created and returned.
	 * If the device ID from the gateway is read out, a GatewayID component is returned.
	 * ORG 0x05 and 0x06 have also one component class.
	 * ORG 0x07 devices are parsed by searching an entry for this device in the known device list.
	 * If an entry is found, the component class will be loaded dynamically. If no entry exists, a ErrorMsg
	 * instance is returned.
	 * 
	 * @param enoceanRawData Raw data frame
	 * @return ErrorMsg for unknown devices, null pointer for other errors and a component instance otherwise
	 * @throws ParserConfigurationException Exception thrown by the parser 
	 * @throws SAXException Exception thrown by the parser 
	 * @throws IOException Exception thrown by the parser 
	 */
	public EnoceanComponent parseEnoceanData(EnoceanRawData enoceanRawData) throws ParserConfigurationException, SAXException, IOException {

		if (listeners == null) return null; //invalid list
		
		try {
			@SuppressWarnings("unused")
			final EnoceanIPData ipData = new EnoceanIPData(enoceanRawData.getDataBytes()); //check header and length
		} catch (EnOceanExceptionDeviceUnknown e) {
			log.error("parser exception: " + e.getMessage());
			return null;
		} catch (EnOceanExceptionFormat e) {
			log.error("parser exception: " + e.getMessage());
			return null;
		} catch (EnOceanExceptionLenght e) {
			log.error("parser exception: " + e.getMessage());
			return null;
		} catch (NullPointerException e) {
			log.error("parser exception: " + e.getMessage());
			return null;
		}

		list = new ArrayList<Device>(); //create device list
		Telegram telegram = new Telegram(enoceanRawData.getString()); //create a new telegram from raw data

		DeviceListXML deviceListXML = new DeviceListXML(devicesFile, options);  //open devices.xml for getting a list

		list = deviceListXML.getList(); 		// readout xml-data into local list

		//EEP 2.1 ORG
		//to be compatible with future devices, the new ORG numbers are changed to the old ones
		//(from EEP 2.1 to EEP2.0)
		if (telegram.getxORG().equals("F6")) {
			telegram.setxORG("05");
		}
		if (telegram.getxORG().equals("D5")) {
			telegram.setxORG("06");
		}
		if (telegram.getxORG().equals("A5")) {
			telegram.setxORG("07");
		}
		
		//response telegram to a gateway ID request, create a new GateWayID "component"
		if (telegram.getxORG().equals("98")) {
			return new GateWayID(telegram, listeners);
		}
		
		//reply for a successfully sent packet
		if (telegram.getxORG().equals("58")) {
			//System.out.println("ACK for sent packet");
		}
		
		log.debug("parser: ORG: " + telegram.getxORG());
		
		//EEP 2.0 ORG
		
		//RPS telegrams
		if (telegram.getxORG().equals("05")) {
			return new EEP_05_xx_xx(telegram, listeners); //create a new ORG 05 component
		}
		
		//1BS telegrams
		if (telegram.getxORG().equals("06")) {
			return new EEP_06_xx_xx(telegram, listeners); //create a new ORG 06 component
		}
		
		//4BS telegrams
		if (telegram.getxORG().equals("07")) {
			if (!telegram.getLRN()) { //check for a set learn bit
				telegram.setFunc(getFuncFromID(telegram.getDeviceID())); //get the FUNC from the database
				telegram.setType(getTypeFromID(telegram.getDeviceID())); //get the TYPE from the database

				Class<?> component; //create a raw class
				try {
					System.out.println("at.technikum_wien.embsys.aat.PriscillaCore.component.EEP_07_" + String.format("%02X", telegram.getFunc()) + "_"
							+ String.format("%02X", telegram.getType()));
					//assemble the class name by ORG, FUNC and TYPE and search for it
					component = Class.forName("at.technikum_wien.embsys.aat.PriscillaCore.component.EEP_07_" + String.format("%02X", telegram.getFunc()) + "_"
							+ String.format("%02X", telegram.getType()));;
							
							
							
					//create a new constructor with the parameters "Telegram" and "List"
					@SuppressWarnings("rawtypes")
					Constructor ctor = component.getConstructor(Telegram.class, List.class);
					
					//create a new instance
					Object o = ctor.newInstance(telegram, listeners);
					
					log.debug("loaded ORG07 class: " + "at.technikum_wien.embsys.aat.PriscillaCore.component.EEP_07_" + String.format("%02X", telegram.getFunc()) + "_"
							+ String.format("%02X", telegram.getType()));

					//return new instance
					return (EnoceanComponent) o;
				} catch (ClassNotFoundException e) { //class not found, check why

					
					if ((telegram.getFunc() == 0) && (telegram.getType() == 0)) {
						//device is not known (FUNC/TYPE = 0)
						log.info("device not known: " + telegram.getDeviceID());
						return new ErrorMsg(telegram, ErrorCode.DEVICE_NOT_KNOWN, "Device " + telegram.getDeviceID() + " not known - press the learn button before!", listeners);
					} else {
						//device does not exist within priscilla
						log.error("device not implemented: "+ "ID: " + telegram.getDeviceID() + ", func: " + telegram.getxFunc() + ", type: " + telegram.getxType());
						return new ErrorMsg(telegram, ErrorCode.COMPONENT_NOT_KNOWN, "Func/Type not correct or component not available!!!! " + "   Func: " + telegram.getFunc()
								+ "     Type: " + telegram.getType() + "          DeviceID: " + telegram.getDeviceID(), listeners);
					}
				} catch (InstantiationException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (IllegalAccessException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (SecurityException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (NoSuchMethodException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (IllegalArgumentException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (InvocationTargetException e) {
					log.error("parser exception: " + e.getMessage());
				}
			} else { // learn Telegram!
				ErrorCodeLRN returnValueLRN =  deviceListXML.createNewXMLEntry(list, telegram); //create the new device in the database
				if (returnValueLRN == ErrorCodeLRN.SUCCESS) {
					 //successfully learned in
					log.info("teach in success");
					return new LearnTelegram(telegram, listeners);
				} else if (returnValueLRN == ErrorCodeLRN.KNOWN_DEVICE) {
					//device is already known (only valid if TeachInOptions is set to ONE_TIME, otherwise the entry will be overwritten)
					return new ErrorMsg(telegram, ErrorCode.TEACHING_ERROR_KNOWN_DEVICE, "ERROR Teaching!     Device " + telegram.getDeviceID() + " already tought!", listeners);
				} else if (returnValueLRN == ErrorCodeLRN.NO_ORG_07_TELEGRAM) {
					//no teach in telegram (wrong transmission)
					return new ErrorMsg(telegram, ErrorCode.TEACHING_ERROR_NO_ORG_07_TELEGRAM, "ERROR Teaching!     Device " + telegram.getDeviceID() + " is not a ORG_7 Device!", listeners);
				} else if (returnValueLRN == ErrorCodeLRN.UNABLE_TO_LEARN) {
					//unable to learn, because sensor does not provide type and func
					return new ErrorMsg(telegram, ErrorCode.TEACHING_ERROR_UNABLE_TO_LEARN, "ERROR Teaching, device does not provide TYPE and FUNC, please update the XML file manually!", listeners);
				} else {
					//unknown error
					return new ErrorMsg(telegram, ErrorCode.UNKNOWN_ERROR, "ERROR Teaching!", listeners);
				}
			}
		}
		return null;
	}

	/**
	 * Parse the given ESP3 Telegram and return the corresponding component
	 * 
	 * This method checks the ESP3 Telegram.
	 * The actual device list is read from the DeviceListXML class.
	 * The ORG byte is read out and different components are created and returned.
	 * If the device ID from the gateway is read out, a GatewayID component is returned.
	 * ORG 0x05 and 0x06 have also one component class.
	 * ORG 0x07 devices are parsed by searching an entry for this device in the known device list.
	 * If an entry is found, the component class will be loaded dynamically. If no entry exists, a ErrorMsg
	 * instance is returned.
	 * 
	 * @param enoceanRawData Raw data frame
	 * @return ErrorMsg for unknown devices, null pointer for other errors and a component instance otherwise
	 * @throws ParserConfigurationException Exception thrown by the parser 
	 * @throws SAXException Exception thrown by the parser 
	 * @throws IOException Exception thrown by the parser 
	 */
	public EnoceanComponent parseEnoceanData(TelegramESP3 enoceanData) throws ParserConfigurationException, SAXException, IOException {

		if (listeners == null) return null; //invalid list

		list = new ArrayList<Device>(); //create device list
		
		Telegram telegram = new Telegram(enoceanData); //create a new telegram from raw data

		DeviceListXML deviceListXML = new DeviceListXML(devicesFile, options);  //open devices.xml for getting a list

		list = deviceListXML.getList(); 		// readout xml-data into local list
		
		//get BASE_ID -> save ID from serial stick
		if (telegram.getxORG().equals("00")) {
			return new GateWayID(telegram, listeners);
		}
		
		//EEP 2.0 ORG
		
		log.debug("parser: ORG: " + telegram.getxORG());
		
		//RPS telegrams
		if (telegram.getxORG().equals("05")) {
			return new EEP_05_xx_xx(telegram, listeners); //create a new ORG 05 component
		}
		
		//1BS telegrams
		if (telegram.getxORG().equals("06")) {
			return new EEP_06_xx_xx(telegram, listeners); //create a new ORG 06 component
		}
		
		//4BS telegrams
		if (telegram.getxORG().equals("07")) {
			if (!telegram.getLRN()) { //check for a set learn bit
				telegram.setFunc(getFuncFromID(telegram.getDeviceID())); //get the FUNC from the database
				telegram.setType(getTypeFromID(telegram.getDeviceID())); //get the TYPE from the database

				Class<?> component; //create a raw class
				try {
					
					//assemble the class name by ORG, FUNC and TYPE and search for it
					component = Class.forName("main.java.technikum.wien.aat.priscilla.component.EEP_07_" + String.format("%02X", telegram.getFunc()) + "_"
							+ String.format("%02X", telegram.getType()));;
							
					
							
					//create a new constructor with the parameters "Telegram" and "List"
					@SuppressWarnings("rawtypes")
					Constructor ctor = component.getConstructor(Telegram.class, List.class);
					
					//create a new instance
					Object o = ctor.newInstance(telegram, listeners);
					
					log.debug("loaded ORG07 class: " + "at.technikum_wien.embsys.aat.PriscillaCore.component.EEP_07_" + String.format("%02X", telegram.getFunc()) + "_"
							+ String.format("%02X", telegram.getType()));
					
					//return new instance
					return (EnoceanComponent) o;
				} catch (ClassNotFoundException e) { //class not found, check why

					
					if ((telegram.getFunc() == 0) && (telegram.getType() == 0)) {
						//device is not known (FUNC/TYPE = 0)
						log.info("device not known: " + telegram.getDeviceID());
						return new ErrorMsg(telegram, ErrorCode.DEVICE_NOT_KNOWN, "Device " + telegram.getDeviceID() + " not known - press the learn button before!", listeners);
					} else {
						//device does not exist within priscilla
						log.error("device not implemented: "+ "ID: " + telegram.getDeviceID() + ", func: " + telegram.getxFunc() + ", type: " + telegram.getxType());
						return new ErrorMsg(telegram, ErrorCode.COMPONENT_NOT_KNOWN, "Func/Type not correct or component not available!!!! " + "   Func: " + telegram.getFunc()
								+ "     Type: " + telegram.getType() + "          DeviceID: " + telegram.getDeviceID(), listeners);
					}
				} catch (InstantiationException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (IllegalAccessException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (SecurityException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (NoSuchMethodException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (IllegalArgumentException e) {
					log.error("parser exception: " + e.getMessage());
				} catch (InvocationTargetException e) {
					log.error("parser exception: " + e.getMessage());
				}
			} else { // learn Telegram!
				ErrorCodeLRN returnValueLRN =  deviceListXML.createNewXMLEntry(list, telegram); //create the new device in the database
				if (returnValueLRN == ErrorCodeLRN.SUCCESS) {
					 //successfully learned in
					log.info("teach in success");
					return new LearnTelegram(telegram, listeners);
				} else if (returnValueLRN == ErrorCodeLRN.KNOWN_DEVICE) {
					//device is already known (only valid if TeachInOptions is set to ONE_TIME, otherwise the entry will be overwritten)
					return new ErrorMsg(telegram, ErrorCode.TEACHING_ERROR_KNOWN_DEVICE, "ERROR Teaching!     Device " + telegram.getDeviceID() + " already tought!", listeners);
				} else if (returnValueLRN == ErrorCodeLRN.NO_ORG_07_TELEGRAM) {
					//no teach in telegram (wrong transmission)
					return new ErrorMsg(telegram, ErrorCode.TEACHING_ERROR_NO_ORG_07_TELEGRAM, "ERROR Teaching!     Device " + telegram.getDeviceID() + " is not a ORG_7 Device!", listeners);
				} else if (returnValueLRN == ErrorCodeLRN.UNABLE_TO_LEARN) {
					//unable to learn, because sensor does not provide type and func
					return new ErrorMsg(telegram, ErrorCode.TEACHING_ERROR_UNABLE_TO_LEARN, "ERROR Teaching, device does not provide TYPE and FUNC, please update the XML file manually!", listeners);
				} else {
					//unknown error
					return new ErrorMsg(telegram, ErrorCode.UNKNOWN_ERROR, "ERROR Teaching!", listeners);
				}
			}
		}
		return null;
	}
	
	/**
	 * Get the FUNC for a device
	 * 
	 * This method walks through the list and returns the FUNC if the
	 * device is in the database, otherwise the returned FUNC is 0x00
	 * 
	 * @param deviceID
	 * @return FUNC of the given device
	 */
	int getFuncFromID(String deviceID) {

		int ID0 = toInt(deviceID.substring(6, 8));
		int ID1 = toInt(deviceID.substring(4, 6));
		int ID2 = toInt(deviceID.substring(2, 4));
		int ID3 = toInt(deviceID.substring(0, 2));

		for (int i = 0; i < list.size(); i++) {
			if (ID0 == list.get(i).id0) {
				if (ID1 == list.get(i).id1) {
					if (ID2 == list.get(i).id2) {
						if (ID3 == list.get(i).id3) {
							return list.get(i).func;
						}
					}
				}
			}
		}
		return 0;
	}

	/**
	 * Get the TYPE for a device
	 * 
	 * This method walks through the list and returns the TYPE if the
	 * device is in the database, otherwise the returned TYPE is 0x00
	 * 
	 * @param deviceID
	 * @return TYPE of the given device
	 */
	int getTypeFromID(String deviceID) {
		int ID0 = toInt(deviceID.substring(6, 8));
		int ID1 = toInt(deviceID.substring(4, 6));
		int ID2 = toInt(deviceID.substring(2, 4));
		int ID3 = toInt(deviceID.substring(0, 2));

		for (int i = 0; i < list.size(); i++) {
			if (ID0 == list.get(i).id0) {
				if (ID1 == list.get(i).id1) {
					if (ID2 == list.get(i).id2) {
						if (ID3 == list.get(i).id3) {
							return (list.get(i).type);
						}
					}
				}
			}
		}

		return 0;
	}

	
	/**
	 * Parse a hex string into an integer value
	 * 
	 * @param ascii Hex formatted string
	 * @return Integer representation
	 */
	int toInt(String ascii) {
		return Integer.parseInt(ascii, 16);
	}


	public void setListeners(List<?> listeners) {
		this.listeners = listeners;
	}
}
